package com.gupao.p7store.common.utils;


import com.gupao.p7store.common.exception.UtilException;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.SecureRandom;


/**
 * 雪花算法
 *
 * @ClassName: SnowflakeIdUtil
 * @Author dengbicheng
 * @Date 2021/11/23 17:48
 * @Version v 1.0
 */
public class SnowflakeIdUtil {

	/**
	 * 基准时间
	 */
	private static long TW_EPOCH = 1550560044132L;

	/**
	 * 区域标志位数
	 */
	private final static long REGION_ID_BITS= 3L;

	/**
	 * 机器标识位数
	 */
	private final static long WORKER_ID_BITS = 10L;

	/**
	 * 序列号识位数
	 */
	private final static long SEQUENCE_BITS = 10L;

	/**
	 * 区域标志ID最大值
	 */
	private final static long MAX_REGION_ID = -1L ^ (-1L << REGION_ID_BITS);

	/**
	 * 机器ID最大值
	 */
	private final static long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);

	/**
	 * 序列号ID最大值
	 */
	private final static long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);

	/**
	 * 机器ID偏左移10位
	 */
	private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;

	/**
	 * 业务ID偏左移20位
	 */
	private final static long REGION_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

	/**
	 * 时间毫秒左移23位
	 */
	private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + REGION_ID_BITS;

	private static long LAST_TIMESTAMP = -1L;

	private static long SEQUENCE = 0L;


	private static int TIME_OFFSET = 5;

	/**
	 * 第几个机器，数值不可用超过1023，从0开始，最多可以扩到第1024个机器
	 */
	private static long WORKER_ID = generateWorkerIdByIpv4();

	/**
	 * 第几个机房 ，数值不可用超过7，从0开始，最多可以扩到8个机房
	 */
	private static long DATA_CENTER_ID = 0L;


	public static long generate() {

		return nextId(false, 0);
	}

	public static String build(){
		return String.valueOf(generate());
	}

	/**
	 * 实际产生代码的
	 *
	 * @param isPadding
	 * @param busId
	 * @return
	 */
	private static synchronized long nextId(boolean isPadding, long busId) {
		long timestamp = timeGen();
		long paddingnum = DATA_CENTER_ID;

		if (isPadding) {
			paddingnum = busId;
		}

		if (timestamp < LAST_TIMESTAMP) {

			long delay = LAST_TIMESTAMP - timestamp;

			// 如果时间偏差较小则等待
			if (delay < TIME_OFFSET) {
				try {
					Thread.sleep(delay);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

				timestamp = timeGen();
			}

			// 如果还没好则抛异常(后续加报警代码实现)
			if (timestamp < LAST_TIMESTAMP) {
				try {
					throw new Exception("Clock moved backwards.  Refusing to generate id for " + (LAST_TIMESTAMP - timestamp) + " milliseconds");
				} catch (Exception e) {
					e.printStackTrace();
				}
			} else {
				return generate();
			}

		}

		// 如果上次生成时间和当前时间相同,在同一毫秒内
		if (LAST_TIMESTAMP == timestamp) {
			// sequence自增，因为sequence只有10bit，所以和sequenceMask相与一下，去掉高位
            SEQUENCE = (SEQUENCE + 1) & SEQUENCE_MASK;
			// 判断是否溢出,也就是每毫秒内超过1024，当为1024时，与sequenceMask相与，sequence就等于0
			if (SEQUENCE == 0) {
				// 自旋等待到下一毫秒
				timestamp = tailNextMillis(LAST_TIMESTAMP);
			}
		} else {
			// 如果和上次生成时间不同,重置sequence，就是下一毫秒开始，sequence计数重新从0开始累加,
			// 为了保证尾数随机性更大一些,最后一位设置一个随机数
            SEQUENCE = new SecureRandom().nextInt(10);
		}

        LAST_TIMESTAMP = timestamp;

		return ((timestamp - TW_EPOCH) << TIMESTAMP_LEFT_SHIFT) | (paddingnum << REGION_ID_SHIFT) | (WORKER_ID << WORKER_ID_SHIFT) | SEQUENCE;
	}

    /**
     * 防止产生的时间比之前的时间还要小（由于NTP回拨等问题）,保持增量的趋势.
     * @param lastTimestamp
     * @return
     */
	private static long tailNextMillis(final long lastTimestamp) {
		long timestamp = timeGen();
		while (timestamp <= lastTimestamp) {
			timestamp = timeGen();
		}
		return timestamp;
	}

    /**
     * 获取当前的时间戳
     * @return
     */
	protected static long timeGen() {
		return System.currentTimeMillis();
	}


    /**
     * ip后两个网段计算，要求前面网段一致
     *
     * @ClassName: SnowflakeIdUtil
     * @Author dengbicheng
     * @Date 2021/11/23 17:48
     * @Version v 1.0
     */
	private static Long generateWorkerIdByIpv4() {
		Long workerId;

		InetAddress address = null;

		try {
			address = InetAddress.getLocalHost();

		} catch (final UnknownHostException e) {
            throw new UtilException("Get ip Exception!!!!");
		}
		byte[] ipAddressByteArray = address.getAddress();

		workerId = (long) (((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF));

		return workerId;
	}
}

